package cn.sc.summer.core.aspect;

import cn.hutool.core.date.DateUtil;
import cn.sc.summer.constant.project.DateConstant;
import cn.sc.summer.core.annotation.AntiShake;
import cn.sc.summer.exception.exceptions.CommonException;
import cn.sc.summer.exception.hodler.SpringContextHolder;
import cn.sc.summer.redis.redisson.RedissonLock;
import cn.sc.summer.redis.util.RedisHelper;
import cn.sc.summer.token.util.AESUtil;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.*;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.beans.factory.DisposableBean;
import org.springframework.stereotype.Component;
import org.springframework.util.CollectionUtils;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;

import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;
import java.lang.reflect.Method;
import java.util.Arrays;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.TimeUnit;

/**
 * 类名：幂等性校验接口AOP实现类
 *
 * @author a-xin
 * @date 2023/7/27 15:53
 */
@Slf4j
@Aspect
@Component
@RequiredArgsConstructor
public class AntiShakeAspect implements DisposableBean {

    /**
     * 当前线程
     */
    private final ThreadLocal<Map<String, Object>> threadLocal = new ThreadLocal<>();

    /**
     * redis端数据map-key对应值
     */
    private static final String KEY = "KEY";

    /**
     * 是否删除key
     */
    private static final String DEL_KEY = "DEL_KEY";

    /**
     * 抛出异常后是否停止校验
     */
    private static final String EXCEPTION_DEL = "EXCEPTION_DEL";

    /**
     * redisKey存储加密密钥
     */
    private static final String AES_KEY = "YCaoQXb3zS8t4nLL";

    /**
     * 存储redis-key前缀
     */
    private static final String IDEMPOTENT_ASPECT_PREFIX = "IDEMPOTENT_ASPECT_PREFIX:";

    @Resource
    private RedissonLock redissonLock;

    @Pointcut("@annotation(cn.sc.summer.core.annotation.AntiShake)")
    public void pointCut() {
    }

    @Before("pointCut()")
    public void beforePointCut(JoinPoint joinPoint) {

        ServletRequestAttributes requestAttributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
        if (null == requestAttributes) {
            throw new CommonException("The request was exceptional, please try again！");
        }
        HttpServletRequest request = requestAttributes.getRequest();

        MethodSignature signature = (MethodSignature) joinPoint.getSignature();
        Method method = signature.getMethod();
        if (!method.isAnnotationPresent(AntiShake.class)) {
            return;
        }
        AntiShake antiShake = method.getAnnotation(AntiShake.class);
        boolean isAntiShake = antiShake.isAntiShake();
        if (!isAntiShake) {
            return;
        }

        HttpServletRequest httpServletRequest = SpringContextHolder.getHttpServletRequest();
        String ip = httpServletRequest.getRemoteHost();
        String url = request.getRequestURL().toString().replace("http://", "").replace("https://", "");
        String argString = Arrays.asList(joinPoint.getArgs()).toString();
        //key主要用于对请求者，请求客户端的请求频次做限制，也可以使用token作为key
        String key = IDEMPOTENT_ASPECT_PREFIX + AESUtil.encrypt(ip + url + argString, AES_KEY);

        long expireTime = antiShake.expireTime();
        String info = antiShake.info();
        TimeUnit timeUnit = antiShake.timeUnit();
        boolean delKey = antiShake.delKey();
        boolean exceptionDel = antiShake.exceptionDel();

        String value = DateUtil.format(new Date(), DateConstant.YYYY_MM_DD_HH_MM_SS_1);

        if (RedisHelper.hasKey(key)) {
            throw new CommonException(info);
        }

        boolean redisLock = redissonLock.tryLock(ip + url + argString, 1000, 1200, TimeUnit.MILLISECONDS);
        if (redisLock) {
            if (RedisHelper.set(key, value, expireTime, timeUnit)) {
                log.info("[idempotent]--->has stored key = {}, value = {}, expireTime = {} {}, now = {}", key, value, expireTime, timeUnit, DateUtil.format(new Date(), "yyyy-MM-dd HH:mm:ss"));
            } else {
                throw new CommonException("Redis service exception！");
            }
        } else {
            throw new CommonException(info);
        }

        Map<String, Object> map = CollectionUtils.isEmpty(threadLocal.get()) ? new HashMap<>(4) : threadLocal.get();
        map.put(KEY, key);
        map.put(DEL_KEY, delKey);
        map.put(EXCEPTION_DEL, exceptionDel);

        threadLocal.set(map);

    }

    @AfterThrowing(value = "pointCut()", throwing = "exception")
    public void afterThrowing(Exception exception) {

        Map<String, Object> map = threadLocal.get();
        String key = map.get(KEY).toString();
        boolean exceptionDel = (boolean) map.get(EXCEPTION_DEL);
        if (exceptionDel) {
            map.put(DEL_KEY, true);
        }

        log.info("[idempotent]--->has throwing an Exception = {}, Exception = {}", key, exception.getMessage());

    }

    @After("pointCut()")
    public void afterPointCut() {

        Map<String, Object> map = threadLocal.get();
        if (CollectionUtils.isEmpty(map)) {
            return;
        }

        String key = map.get(KEY).toString();
        boolean delKey = (boolean) map.get(DEL_KEY);

        if (delKey) {
            RedisHelper.del(map.get(KEY).toString());
            log.info("[idempotent]--->has removed key = {}", key);
        }

        threadLocal.remove();

    }

    /**
     * Invoked by the containing {@code BeanFactory} on destruction of a bean.
     *
     */
    @Override
    public void destroy() {
        threadLocal.remove();
    }

}

